﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;

namespace GraphLight.Controls
{
    public partial class DragDropManager
    {
        private static readonly IDictionary<FrameworkElement, Subscription> _subscriptions;
        private static readonly MouseButtonEventHandler _mouseLeftButtonDownEventHandler;
        private static DragDropOptions _options;

        static DragDropManager()
        {
            _subscriptions = new Dictionary<FrameworkElement, Subscription>();
            _mouseLeftButtonDownEventHandler = onMouseLeftButtonDown;
        }

        #region AllowDrag

        public static readonly DependencyProperty AllowDragProperty = DependencyProperty.RegisterAttached(
            "AllowDrag", typeof(bool), typeof(DragDropManager), new PropertyMetadata(false, onAllowDragPropertyChanged));

        public static void SetAllowDrag(DependencyObject element, bool value)
        {
            element.SetValue(AllowDragProperty, value);
        }

        public static bool GetAllowDrag(DependencyObject element)
        {
            return (bool)element.GetValue(AllowDragProperty);
        }

        private static void onAllowDragPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
        }

        #endregion

        #region AllowDrop

        public static readonly DependencyProperty AllowDropProperty = DependencyProperty.RegisterAttached(
            "AllowDrop", typeof(bool), typeof(DragDropManager), new PropertyMetadata(false, onAllowDropPropertyChanged));

        public static void SetAllowDrop(DependencyObject element, bool value)
        {
            element.SetValue(AllowDropProperty, value);
        }

        public static bool GetAllowDrop(DependencyObject element)
        {
            return (bool)element.GetValue(AllowDropProperty);
        }

        private static void onAllowDropPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
        }

        #endregion

        public static void AddDragQueryHandler(FrameworkElement element, Func<IDragDropOptions, bool> handler)
        {
            Subscription subscription;
            if (!_subscriptions.TryGetValue(element, out subscription))
            {
                subscription = new Subscription(element);
                _subscriptions.Add(element, subscription);
            }
            subscription.DragQuery = handler;
            element.RemoveHandler(UIElement.MouseLeftButtonDownEvent, _mouseLeftButtonDownEventHandler);
            element.AddHandler(UIElement.MouseLeftButtonDownEvent, _mouseLeftButtonDownEventHandler, true);
        }

        public static void AddDropQueryHandler(FrameworkElement element, Func<IDragDropOptions, bool> handler)
        {
            Subscription subscription;
            if (!_subscriptions.TryGetValue(element, out subscription))
            {
                subscription = new Subscription(element);
                _subscriptions.Add(element, subscription);
            }
            subscription.DropQuery = handler;
            element.RemoveHandler(UIElement.MouseLeftButtonDownEvent, _mouseLeftButtonDownEventHandler);
            element.AddHandler(UIElement.MouseLeftButtonDownEvent, _mouseLeftButtonDownEventHandler, true);
        }

        public static void AddDropInfoHandler(FrameworkElement element, Action<IDragDropOptions> handler)
        {
            Subscription subscription;
            if (!_subscriptions.TryGetValue(element, out subscription))
            {
                subscription = new Subscription(element);
                _subscriptions.Add(element, subscription);
            }
            subscription.DropInfo = handler;
            element.RemoveHandler(UIElement.MouseLeftButtonDownEvent, _mouseLeftButtonDownEventHandler);
            element.AddHandler(UIElement.MouseLeftButtonDownEvent, _mouseLeftButtonDownEventHandler, true);
        }

        public static void RemoveAllHandlers(FrameworkElement element)
        {
            Subscription subscription;
            if (!_subscriptions.TryGetValue(element, out subscription))
                return;
            subscription.DropQuery = null;
            subscription.DropInfo = null;
            _subscriptions.Remove(element);
            element.RemoveHandler(UIElement.MouseLeftButtonDownEvent, _mouseLeftButtonDownEventHandler);
        }

        #region Mouse handlers

        private static void onMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var subscription = getSubscription(sender);
            if (subscription == null)
                return;

            var sourceElement = getDragSource(e.OriginalSource);
            if (sourceElement == null)
                return;

            var options = new DragDropOptions
                {
                    Source = sourceElement,
                    Start = e.GetPosition(null),
                };

            // Query if drag is possible
            if (subscription.DragQuery != null)
                if (!subscription.DragQuery(options))
                    return;

            if (!sourceElement.CaptureMouse())
                throw new Exception("Не удалось захватить мышь");

            foreach (var element in _subscriptions.Keys)
            {
                element.MouseMove += onMouseMove;
                element.LostMouseCapture += onLostMouseCapture;
                element.MouseLeftButtonUp += onMouseLeftButtonUp;
            }

            _options = options;
        }

        private static void onMouseMove(object sender, MouseEventArgs e)
        {
            _options.Status = DragDropStatus.Dragging;
            publishDropInfo(sender, e);
        }

        private static void onMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            var element = e.OriginalSource as UIElement;
            if(element!=null)
                element.ReleaseMouseCapture();
        }

        private static void onLostMouseCapture(object sender, MouseEventArgs e)
        {
            foreach (var element in _subscriptions.Keys)
            {
                element.MouseMove -= onMouseMove;
                element.LostMouseCapture -= onLostMouseCapture;
                element.MouseLeftButtonUp -= onMouseLeftButtonUp;
            }
            _options.Status = DragDropStatus.Completed;
            publishDropInfo(sender, e);
            _options = null;
        }

        private static void publishDropInfo(object sender, MouseEventArgs e)
        {
            var subscription = getSubscription(sender);
            if (subscription == null)
                return;

            var current = e.GetPosition(null);
            _options.Current = current;

            if (subscription.DropQuery != null && !subscription.DropQuery(_options))
                return;

            var target = getDropTarget(subscription.Subscriber);
            if (target == null || !GetAllowDrop(target))
                return;

            _options.Target = (FrameworkElement)target;
            _options.Relative = e.GetPosition(subscription.Subscriber);
            subscription.DropInfo(_options);
        }

        #endregion

        /// <summary>
        /// Find nearest element in visual tree labeld with AllowDrag=true.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        private static FrameworkElement getDragSource(object obj)
        {
            var element = obj as FrameworkElement;
            if (element == null || GetAllowDrop(element))
                return null;
            if (GetAllowDrag(element))
                return element;
            return getDragSource(VisualTreeHelper.GetParent(element));
        }

        private static Subscription getSubscription(object key)
        {
            var element = key as FrameworkElement;
            Subscription subscription;
            return element != null && _subscriptions.TryGetValue(element, out subscription)
                ? subscription
                : null;
        }

        private class Subscription
        {
            public Subscription(FrameworkElement subscriber)
            {
                Subscriber = subscriber;
            }

            public Func<IDragDropOptions, bool> DragQuery;
            public Func<IDragDropOptions, bool> DropQuery;
            public Action<IDragDropOptions> DropInfo;
            public readonly FrameworkElement Subscriber;
        }

        private sealed class DragDropOptions : IDragDropOptions
        {
            public DragDropOptions()
            {
                Status = DragDropStatus.NotStarted;
            }

            public FrameworkElement Source { get; internal set; }
            public FrameworkElement Target { get; internal set; }
            public object Payload { get; set; }
            public Point Start { get; internal set; }
            public Point Current { get; internal set; }
            public Point Relative { get; internal set; }
            public double DeltaX { get { return Current.X - Start.X; } }
            public double DeltaY { get { return Current.Y - Start.Y; } }
            public DragDropStatus Status { get; internal set; }
        }
    }

    public interface IDragDropOptions
    {
        FrameworkElement Source { get; }
        FrameworkElement Target { get; }
        object Payload { get; set; }
        Point Start { get; }
        Point Current { get; }
        Point Relative { get; }
        double DeltaX { get; }
        double DeltaY { get; }
    }

    public enum DragDropStatus
    {
        NotStarted,
        Dragging,
        Completed,
    }
}